1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.filesystem.hipfs;
12 
13 public import hip.api.filesystem.hipfs;
14 import hip.util.reflection;
15 
16 /** 
17  * Returns whether if the path attempts to exit the initial one.
18  * Params:
19  *   initial = 
20  *   toAppend = 
21  * Returns: 
22  */
23 private pure bool validatePath(string initial, string toAppend)
24 {
25     import hip.util.array:lastIndexOf;
26     import hip.util.string:splitRange, PathString;
27     import hip.util.system : sanitizePath;
28 
29     if(initial.length != 0 && initial[$-1] == '/')
30         initial = initial[0..$-1];
31     scope string appends = toAppend.sanitizePath;
32 
33     PathString newPath = PathString(initial.sanitizePath);
34 
35     foreach(a; splitRange(appends, "/"))
36     {
37         if(a == "" || a == ".")
38             continue;
39         if(a == "..")
40         {
41             long lastInd = newPath.toString.lastIndexOf('/');
42             if(lastInd == -1)
43                 continue;
44             newPath = newPath[0..cast(uint)lastInd];
45         }
46         else
47         {
48             newPath~= "/";
49             newPath~= a;
50         }
51     }
52     for(int i = 0; i < initial.length; i++)
53         if(initial[i] != newPath[i])
54             return false;
55     return true;
56 }
57 
58 ///Function is implemented AppDelegate.m
59 version(AppleOS)
60 private extern(C) const(char*) hipGetResourcesPath();
61 
62 abstract class HipFile : IHipFileItf
63 {
64     immutable FileMode mode;
65     immutable string path;
66     ulong size;
67     ulong cursor;
68     @disable this();
69     this(string path, FileMode mode)
70     {
71         this.mode = mode;
72         this.path = path;
73         open(path, mode);
74         this.size = getSize();
75     }
76     ///Whence is the same from libc
77     long seek(long count, int whence = SEEK_CUR)
78     {
79         switch(whence)
80         {
81             default:
82             case SEEK_CUR:
83                 cursor+= count;
84                 break;
85             case SEEK_END:
86                 cursor = size + count;
87                 break;
88             case SEEK_SET:
89                 cursor = count;
90                 break;
91         }
92         return cast(long)cursor;
93     }
94 
95     T[] rawRead(T)(T[] buffer)
96     {
97         read(cast(void*)buffer.ptr,buffer.length);
98         return buffer;
99     }
100 }
101 
102 
103 class HipFSPromise : IHipFSPromise
104 {
105     string filename;
106     FileReadResult delegate(in ubyte[] data)[] onSuccessList;
107     void delegate(string err)[] onErrorList;
108     ubyte[] data;
109     bool finished = false;
110     FileReadResult result = FileReadResult.keep;
111     this(string filename){this.filename = filename;}
112     IHipFSPromise addOnSuccess(FileReadResult delegate(in ubyte[] data) onSuccess)
113     {
114         if(result != FileReadResult.keep)
115             throw new Exception("HipFSPromise Error: "~filename~" data was already freed, but addOnSuccess is being called.");
116         if(finished)
117         {
118             if(onSuccess(data) == FileReadResult.free)
119             {
120                 result = FileReadResult.free;
121                 dispose();
122             }
123         }
124         else
125             onSuccessList~=onSuccess;
126         return this;
127     }
128     IHipFSPromise addOnError(void delegate(string error) onError)
129     {
130         if(finished)
131         {
132             if(data.length == 0 && result != FileReadResult.free)
133                 onError("No data");
134         }
135         else
136             onErrorList~= onError;
137         return this;
138     }
139     FileReadResult setFinished(ubyte[] data)
140     {
141         if(finished)
142             assert(false, "HipFSPromise was already resolved.");
143         this.data = data;
144         this.finished = true;
145         FileReadResult r = FileReadResult.keep;
146         if(data) foreach(success; onSuccessList)
147             r|= success(data);
148         else foreach(err; onErrorList)
149             err("Could not read file");
150 
151         if(r == FileReadResult.free)
152             dispose();
153         return result = r;
154     }
155     bool resolved() const{return finished;}
156 
157     void dispose()
158     {
159         version(WebAssembly) {}
160         else
161         {
162             import core.memory;
163             GC.free(data.ptr);
164             data = null;
165         }
166     }
167 }
168 
169 /**
170 * FileSystem access for specific platforms.
171 */
172 class HipFileSystemImplementation : IHipFS
173 {
174     protected string defPath;
175     protected string initialPath = "";
176     protected string combinedPath;
177     protected bool isInstalled;
178     protected IHipFileSystemInteraction fs;
179     protected size_t filesReadingCount = 0;
180 
181     protected bool function(string path, out string errMessage)[] extraValidations;
182 
183     version(Android){import hip.filesystem.systems.android;}
184     else version(UWP){import hip.filesystem.systems.uwp;}
185     else version(WebAssembly){import hip.filesystem.systems.browser;}
186     else version(PSVita){import hip.filesystem.systems.cstd;}
187     else version(CustomRuntimeTest){import hip.filesystem.systems.cstd;}
188     else version(HipDStdFile){import hip.filesystem.systems.dstd;}
189     else {import hip.filesystem.systems.cstd;}
190 
191     public void initializeAbsolute()
192     {
193         if(fs is null)
194         {
195             version(Android){fs = new HipAndroidFileSystemInteraction();}
196             else version(UWP){fs = new HipUWPileSystemInteraction();}
197             else version(PSVita){fs = new HipCStdioFileSystemInteraction();}
198             else version(CustomRuntimeTest){fs = new HipCStdioFileSystemInteraction();}
199             else version(WebAssembly){fs = new HipBrowserFileSystemInteraction();}
200             else
201             {
202                 version(HipDStdFile){}else{static assert(false, "HipDStdFile should be marked to be used.");}
203                 fs = new HipStdFileSystemInteraction();
204             }
205         }
206     }
207  
208     
209     public void install(string path)
210     {
211         import hip.util.system : sanitizePath;
212         if(!isInstalled)
213         {
214             initialPath = path.sanitizePath;
215             setPath("");
216             isInstalled = true;
217         }
218     }
219     /**
220     *   This function may be refactored in future since having different
221     *   directories to resources to writeable paths is becoming more common
222     */
223     version(AppleOS)
224     public string getResourcesPath()
225     {
226         import core.stdc.string;
227         auto str = hipGetResourcesPath;
228         return cast(string)str[0..strlen(str)];
229     }
230 
231     
232     public void install(string path,
233     bool function(string path, out string errMessage)[] validations ...)
234     {
235         import hip.util.system : sanitizePath;
236         if(!isInstalled)
237         {
238             install(path);
239             foreach (v; validations){extraValidations~=v;}
240         }
241     }
242     public string getPath(string path)
243     {
244         import hip.util.path:joinPath;
245         import hip.util.system : sanitizePath;
246         import hip.console.log;
247 
248         if(combinedPath)
249             return joinPath(combinedPath, path.sanitizePath);
250         return path.sanitizePath;
251     }
252     public bool isPathValidExtra(string path)
253     {
254         import hip.error.handler;
255         import hip.util.system : sanitizePath;
256         path = path.sanitizePath;
257         string err;
258         foreach (bool function(string, out string) validation; extraValidations)
259         {
260             if(!validation(path, err))
261             {
262                 ErrorHandler.showErrorMessage("HipFileSystem validation error",
263                 "Path '"~path~"' failed at validation with error: '"~err~"'.");
264                 return false;
265             }
266         }
267         return true;
268     }
269     
270     public bool isPathValid(string path, bool expectsFile = true, bool shouldVerify = true)
271     {
272         import hip.error.handler;
273         import hip.util.string;
274         if(!isInstalled) return false;
275         PathString s = PathString(defPath, path);
276         if(!validatePath(initialPath, s.toString))
277         {
278             ErrorHandler.showErrorMessage("Path failed default validation: can't reference external path.", path);
279             return false;
280         }
281         if(shouldVerify)
282         {
283             if((expectsFile && !HipFS.absoluteIsFile(path)) || (!expectsFile && !HipFS.absoluteIsDir(path)))
284             {
285                 ErrorHandler.showErrorMessage("Path failed default validation: Expected '"~ (expectsFile ? "file" : "directory") ~ 
286                 "' but received "~ (expectsFile ? "'directory'" : "'file'"), path);
287                 return false;
288             }
289         }
290 
291         return isPathValidExtra(path);
292     }
293 
294     public bool setPath(string path)
295     {
296         import hip.util.path:joinPath;
297         import hip.util.system : sanitizePath;
298         import hip.console.log;
299         if(path)
300         {
301             defPath = path.sanitizePath;
302             combinedPath = joinPath(initialPath, defPath);
303         }
304         else
305             combinedPath = initialPath;
306         return validatePath(initialPath, combinedPath);
307     }
308 
309     private void defaultErrorHandler(string err = "")
310     {
311         import hip.error.handler;
312         filesReadingCount--;
313         ErrorHandler.assertExit(false, "HipFS Error: "~err);
314     }
315 
316     ///TODO: Fix API. It currently does not work with sync and async at the same way.
317     /// It needs to specify both onSuccess and onError before being able to establish if it is possible to keep or not the memory.
318     public IHipFSPromise read(string path)
319     {
320         import hip.console.log;
321         hiplog("Required path ", getPath(path));
322         path = getPath(path);
323         if(!isPathValid(path))
324         {
325             hiplog("Invalid path ",path," received.");
326             return null;
327         }
328         filesReadingCount++;
329 
330         HipFSPromise promise = new HipFSPromise(path);
331 
332         fs.read(path, (ubyte[] data)
333         {
334             filesReadingCount--;
335             return promise.setFinished(data);
336         }, (string err)
337         {
338             promise.setFinished(null);
339             defaultErrorHandler(err);
340         });
341         
342         return promise;
343     }
344 
345     public IHipFSPromise readText(string path)
346     {
347         IHipFSPromise ret = read(path);
348         // if(ret)
349         // {
350         //     import std.utf;
351         //     output = toUTF8((cast(string)data));
352         // }
353         return ret;
354     }
355     
356     public bool write(string path, const(void)[] data)
357     {
358         if(!isPathValid(path))
359             return false;
360         return fs.write(getPath(path), data);
361     }
362 
363 
364     public bool exists(string path){return isPathValid(path) && fs.exists(getPath(path));}
365     public bool remove(string path)
366     {
367         if(!isPathValid(path))
368             return false;
369         return fs.remove(getPath(path));
370     }
371 
372     public string getcwd()
373     {
374         return getPath("");
375     }
376 
377     public bool absoluteExists(string path){return fs.exists(path);}
378     public bool absoluteIsDir(string path){return fs.isDir(path);}
379     public bool absoluteIsFile(string path){return fs.isFile(path);}
380     public bool absoluteRemove(string path){return fs.remove(path);}
381     public bool absoluteWrite(string path, const(void)[] data){return fs.write(path, data);}
382     public bool absoluteRead(string path, out void[] output)
383     {
384         ///This may need to be refactored in the future.
385         // import std.functional:toDelegate;
386         return fs.read(path, (void[] data){output = data; return FileReadResult.keep;}, (err) => defaultErrorHandler(err));
387     }
388     @ExportD("ubyte") public bool absoluteRead(string path, out ubyte[] output)
389     {
390         void[] data;
391         bool ret = absoluteRead(path, data);
392         output = cast(ubyte[])data;
393         return ret;
394     }
395 
396     public bool absoluteReadText(string path, out string output)
397     {
398         void[] data;
399         bool ret = absoluteRead(path, data);
400         if(ret)
401             output = cast(string)data;
402         return ret;
403     }
404 
405 
406     public bool isDir(string path){return isPathValid(path, false, false) && fs.isDir(getPath(path));}
407     public bool isFile(string path){return isPathValid(path, true, false) && fs.isFile(getPath(path));}
408 
409     public string writeCache(string cacheName, void[] data)
410     {
411         import hip.util.path:joinPath;
412         string p = joinPath(initialPath, ".cache", cacheName);
413         write(p, data);
414         return p;
415     }
416 }
417 
418 HipFileSystemImplementation HipFileSystem()
419 {
420     __gshared HipFileSystemImplementation fs;
421     if(!fs)
422         fs = new HipFileSystemImplementation();
423     return fs;
424 }
425 
426 export extern(C) IHipFS HipFileSystemAPI()
427 {
428     return HipFileSystem();
429 }
430 
431 alias HipFS = HipFileSystem;